/****************************************************************************
 **
 ** Copyright (C) Qxt Foundation. Some rights reserved.
 **
 ** This file is part of the QxtWeb module of the Qxt library.
 **
 ** This library is free software; you can redistribute it and/or modify it
 ** under the terms of the Common Public License, version 1.0, as published
 ** by IBM, and/or under the terms of the GNU Lesser General Public License,
 ** version 2.1, as published by the Free Software Foundation.
 **
 ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
 ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
 ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
 ** FITNESS FOR A PARTICULAR PURPOSE.
 **
 ** You should have received a copy of the CPL and the LGPL along with this
 ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
 ** included with the source distribution for more information.
 ** If you did not receive a copy of the licenses, contact the Qxt Foundation.
 **
 ** <http://libqxt.org>  <foundation@libqxt.org>
 **
 ****************************************************************************/

/*!
 * \class QxtSmtp
 * \inmodule QxtNetwork
 * \brief The QxtSmtp class implements the SMTP protocol for sending email
 */

#include "qxtsmtp.h"

#include "qxthmac.h"
#include "qxtsmtp_p.h"

#include <QNetworkInterface>
#include <QSslSocket>
#include <QStringList>
#include <QTcpSocket>

QxtSmtpPrivate::QxtSmtpPrivate() : QObject(0)
{
    // empty ctor
}

QxtSmtp::QxtSmtp(QObject *parent) : QObject(parent)
{
    QXT_INIT_PRIVATE(QxtSmtp);
    qxt_d().state = QxtSmtpPrivate::Disconnected;
    qxt_d().nextID = 0;
    qxt_d().socket = new QSslSocket(this);
    QObject::connect(socket(), SIGNAL(encrypted()), this, SIGNAL(encrypted()));
    // QObject::connect(socket(), SIGNAL(encrypted()), &qxt_d(), SLOT(ehlo()));
    QObject::connect(socket(), SIGNAL(connected()), this, SIGNAL(connected()));
    QObject::connect(socket(), SIGNAL(disconnected()), this, SIGNAL(disconnected()));
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
    QObject::connect(socket(), SIGNAL(errorOccurred(QAbstractSocket::SocketError)), &qxt_d(),
                     SLOT(socketError(QAbstractSocket::SocketError)));
#else
    QObject::connect(socket(), SIGNAL(error(QAbstractSocket::SocketError)), &qxt_d(),
                     SLOT(socketError(QAbstractSocket::SocketError)));
#endif
    QObject::connect(this, SIGNAL(authenticated()), &qxt_d(), SLOT(sendNext()));
    QObject::connect(socket(), SIGNAL(readyRead()), &qxt_d(), SLOT(socketRead()));
}

QByteArray QxtSmtp::username() const
{
    return qxt_d().username;
}

void QxtSmtp::setUsername(const QByteArray &username)
{
    qxt_d().username = username;
}

QByteArray QxtSmtp::password() const
{
    return qxt_d().password;
}

void QxtSmtp::setPassword(const QByteArray &password)
{
    qxt_d().password = password;
}

int QxtSmtp::send(const QxtMailMessage &message)
{
    int messageID = ++qxt_d().nextID;
    qxt_d().pending.append(qMakePair(messageID, message));
    if (qxt_d().state == QxtSmtpPrivate::Waiting)
        qxt_d().sendNext();
    return messageID;
}

int QxtSmtp::pendingMessages() const
{
    return qxt_d().pending.count();
}

QTcpSocket *QxtSmtp::socket() const
{
    return qxt_d().socket;
}

void QxtSmtp::connectToHost(const QString &hostName, quint16 port)
{
    qxt_d().useSecure = false;
    qxt_d().state = QxtSmtpPrivate::StartState;
    socket()->connectToHost(hostName, port);
}

void QxtSmtp::connectToHost(const QHostAddress &address, quint16 port)
{
    connectToHost(address.toString(), port);
}

void QxtSmtp::disconnectFromHost()
{
    socket()->disconnectFromHost();
}

bool QxtSmtp::startTlsDisabled() const
{
    return qxt_d().disableStartTLS;
}

void QxtSmtp::setStartTlsDisabled(bool disable)
{
    qxt_d().disableStartTLS = disable;
}

QSslSocket *QxtSmtp::sslSocket() const
{
    return qxt_d().socket;
}

void QxtSmtp::connectToSecureHost(const QString &hostName, quint16 port)
{
    qxt_d().useSecure = true;
    qxt_d().state = QxtSmtpPrivate::StartState;
    sslSocket()->connectToHostEncrypted(hostName, port);
}

void QxtSmtp::connectToSecureHost(const QHostAddress &address, quint16 port)
{
    connectToSecureHost(address.toString(), port);
}

bool QxtSmtp::hasExtension(const QString &extension)
{
    return qxt_d().extensions.contains(extension);
}

QString QxtSmtp::extensionData(const QString &extension)
{
    return qxt_d().extensions[extension];
}

void QxtSmtpPrivate::socketError(QAbstractSocket::SocketError err)
{
    if (err == QAbstractSocket::SslHandshakeFailedError) {
        emit qxt_p().encryptionFailed();
        emit qxt_p().encryptionFailed(socket->errorString().toLatin1());
    } else if (state == StartState) {
        emit qxt_p().connectionFailed();
        emit qxt_p().connectionFailed(socket->errorString().toLatin1());
    }
}

void QxtSmtpPrivate::socketRead()
{
    buffer += socket->readAll();
    while (true) {
        int pos = buffer.indexOf("\r\n");
        if (pos < 0)
            return;
        QByteArray line = buffer.left(pos);
        buffer = buffer.mid(pos + 2);
        QByteArray code = line.left(3);
        switch (state) {
            case StartState:
                if (code[0] != '2') {
                    socket->disconnectFromHost();
                } else {
                    ehlo();
                }
                break;
            case HeloSent:
            case EhloSent:
            case EhloGreetReceived:
                parseEhlo(code, (line[3] != ' '), line.mid(4));
                break;
            case StartTLSSent:
                if (code == "220") {
                    socket->startClientEncryption();
                    ehlo();
                } else {
                    authenticate();
                }
                break;
            case AuthRequestSent:
            case AuthUsernameSent:
                if (authType == AuthPlain)
                    authPlain();
                else if (authType == AuthLogin)
                    authLogin();
                else
                    authCramMD5(line.mid(4));
                break;
            case AuthSent:
                if (code[0] == '2') {
                    state = Authenticated;
                    emit qxt_p().authenticated();
                } else {
                    state = Disconnected;
                    emit qxt_p().authenticationFailed();
                    emit qxt_p().authenticationFailed(line);
                    emit socket->disconnectFromHost();
                }
                break;
            case MailToSent:
            case RcptAckPending:
                if (code[0] != '2') {
                    emit qxt_p().mailFailed(pending.first().first, code.toInt());
                    emit qxt_p().mailFailed(pending.first().first, code.toInt(), line);
                    // pending.removeFirst();
                    // DO NOT remove it, the body sent state needs this message to assigned the next mail failed message
                    // that will the sendNext a reset will be sent to clear things out
                    sendNext();
                    state = BodySent;
                } else
                    sendNextRcpt(code, line);
                break;
            case SendingBody:
                sendBody(code, line);
                break;
            case BodySent:
                if (pending.count()) {
                    // if you removeFirst in RcpActpending/MailToSent on an error, and the queue is now empty,
                    // you will get into this state and then crash because no check is done.  CHeck added but shouldnt
                    // be necessary since I commented out the removeFirst
                    if (code[0] != '2') {
                        emit qxt_p().mailFailed(pending.first().first, code.toInt());
                        emit qxt_p().mailFailed(pending.first().first, code.toInt(), line);
                    } else
                        emit qxt_p().mailSent(pending.first().first);
                    pending.removeFirst();
                }
                sendNext();
                break;
            case Resetting:
                if (code[0] != '2') {
                    emit qxt_p().connectionFailed();
                    emit qxt_p().connectionFailed(line);
                } else {
                    state = Waiting;
                    sendNext();
                }
                break;
            case Disconnected:
            case EhloExtensionsReceived:
            case EhloDone:
            case Authenticated:
            case Waiting:
                // only to make compiler happy
                break;
        }
    }
}

void QxtSmtpPrivate::ehlo()
{
    QByteArray address = "127.0.0.1";
    for (const QHostAddress &addr : QNetworkInterface::allAddresses()) {
        if (addr == QHostAddress::LocalHost || addr == QHostAddress::LocalHostIPv6)
            continue;
        address = addr.toString().toLatin1();
        break;
    }
    socket->write("ehlo " + address + "\r\n");
    extensions.clear();
    state = EhloSent;
}

void QxtSmtpPrivate::parseEhlo(const QByteArray &code, bool cont, const QString &line)
{
    if (code != "250") {
        // error!
        if (state != HeloSent) {
            // maybe let's try HELO
            socket->write("helo\r\n");
            state = HeloSent;
        } else {
            // nope
            socket->write("QUIT\r\n");
            socket->flush();
            socket->disconnectFromHost();
        }
        return;
    } else if (state != EhloGreetReceived) {
        if (!cont) {
            // greeting only, no extensions
            state = EhloDone;
        } else {
            // greeting followed by extensions
            state = EhloGreetReceived;
            return;
        }
    } else {
        extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1);
        if (!cont)
            state = EhloDone;
    }
    if (state != EhloDone)
        return;
    if (extensions.contains("STARTTLS") && !disableStartTLS) {
        startTLS();
    } else {
        authenticate();
    }
}

void QxtSmtpPrivate::startTLS()
{
    socket->write("starttls\r\n");
    state = StartTLSSent;
}

void QxtSmtpPrivate::authenticate()
{
    if (!extensions.contains("AUTH") || username.isEmpty() || password.isEmpty()) {
        state = Authenticated;
        emit qxt_p().authenticated();
    } else {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
        QStringList auth = extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts);
#else
        QStringList auth = extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts);
#endif
        if (auth.contains("CRAM-MD5")) {
            authCramMD5();
        } else if (auth.contains("PLAIN")) {
            authPlain();
        } else if (auth.contains("LOGIN")) {
            authLogin();
        } else {
            state = Authenticated;
            emit qxt_p().authenticated();
        }
    }
}

void QxtSmtpPrivate::authCramMD5(const QByteArray &challenge)
{
    if (state != AuthRequestSent) {
        socket->write("auth cram-md5\r\n");
        authType = AuthCramMD5;
        state = AuthRequestSent;
    } else {
        QxtHmac hmac(QCryptographicHash::Md5);
        hmac.setKey(password);
        hmac.addData(QByteArray::fromBase64(challenge));
        QByteArray response = username + ' ' + hmac.result().toHex();
        socket->write(response.toBase64() + "\r\n");
        state = AuthSent;
    }
}

void QxtSmtpPrivate::authPlain()
{
    if (state != AuthRequestSent) {
        socket->write("auth plain\r\n");
        authType = AuthPlain;
        state = AuthRequestSent;
    } else {
        QByteArray auth;
        auth += '\0';
        auth += username;
        auth += '\0';
        auth += password;
        socket->write(auth.toBase64() + "\r\n");
        state = AuthSent;
    }
}

void QxtSmtpPrivate::authLogin()
{
    if (state != AuthRequestSent && state != AuthUsernameSent) {
        socket->write("auth login\r\n");
        authType = AuthLogin;
        state = AuthRequestSent;
    } else if (state == AuthRequestSent) {
        socket->write(username.toBase64() + "\r\n");
        state = AuthUsernameSent;
    } else {
        socket->write(password.toBase64() + "\r\n");
        state = AuthSent;
    }
}

static QByteArray qxt_extract_address(const QString &address)
{
    int parenDepth = 0;
    int addrStart = -1;
    bool inQuote = false;
    int ct = address.length();

    for (int i = 0; i < ct; i++) {
        QChar ch = address[i];
        if (inQuote) {
            if (ch == '"')
                inQuote = false;
        } else if (addrStart != -1) {
            if (ch == '>')
                return address.mid(addrStart, (i - addrStart)).toLatin1();
        } else if (ch == '(') {
            parenDepth++;
        } else if (ch == ')') {
            parenDepth--;
            if (parenDepth < 0)
                parenDepth = 0;
        } else if (ch == '"') {
            if (parenDepth == 0)
                inQuote = true;
        } else if (ch == '<') {
            if (!inQuote && parenDepth == 0)
                addrStart = i + 1;
        }
    }
    return address.toLatin1();
}

void QxtSmtpPrivate::sendNext()
{
    if (state == Disconnected) {
        // leave the mail in the queue if not ready to send
        return;
    }

    if (pending.isEmpty()) {
        // if there are no additional mails to send, finish up
        state = Waiting;
        emit qxt_p().finished();
        return;
    }

    if (state != Waiting) {
        state = Resetting;
        socket->write("rset\r\n");
        return;
    }
    const QxtMailMessage &msg = pending.first().second;
    rcptNumber = rcptAck = mailAck = 0;
    recipients =
        msg.recipients(QxtMailMessage::To) + msg.recipients(QxtMailMessage::Cc) + msg.recipients(QxtMailMessage::Bcc);
    if (recipients.count() == 0) {
        // can't send an e-mail with no recipients
        emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients);
        emit qxt_p().mailFailed(pending.first().first, QxtSmtp::NoRecipients, QByteArray("e-mail has no recipients"));
        pending.removeFirst();
        sendNext();
        return;
    }
    // We explicitly use lowercase keywords because for some reason gmail
    // interprets any string starting with an uppercase R as a request
    // to renegotiate the SSL connection.
    socket->write("mail from:<" + qxt_extract_address(msg.sender()) + ">\r\n");
    if (extensions.contains("PIPELINING")) // almost all do nowadays
    {
        for (const QString &rcpt : recipients) {
            socket->write("rcpt to:<" + qxt_extract_address(rcpt) + ">\r\n");
        }
        state = RcptAckPending;
    } else {
        state = MailToSent;
    }
}

void QxtSmtpPrivate::sendNextRcpt(const QByteArray &code, const QByteArray &line)
{
    int messageID = pending.first().first;
    const QxtMailMessage &msg = pending.first().second;

    if (code[0] != '2') {
        // on failure, emit a warning signal
        if (!mailAck) {
            emit qxt_p().senderRejected(messageID, msg.sender());
            emit qxt_p().senderRejected(messageID, msg.sender(), line);
        } else {
            emit qxt_p().recipientRejected(messageID, msg.sender());
            emit qxt_p().recipientRejected(messageID, msg.sender(), line);
        }
    } else if (!mailAck) {
        mailAck = true;
    } else {
        rcptAck++;
    }

    if (rcptNumber == recipients.count()) {
        // all recipients have been sent
        if (rcptAck == 0) {
            // no recipients were considered valid
            emit qxt_p().mailFailed(messageID, code.toInt());
            emit qxt_p().mailFailed(messageID, code.toInt(), line);
            pending.removeFirst();
            sendNext();
        } else {
            // at least one recipient was acknowledged, send mail body
            socket->write("data\r\n");
            state = SendingBody;
        }
    } else if (state != RcptAckPending) {
        // send the next recipient unless we're only waiting on acks
        socket->write("rcpt to:<" + qxt_extract_address(recipients[rcptNumber]) + ">\r\n");
        rcptNumber++;
    } else {
        // If we're only waiting on acks, just count them
        rcptNumber++;
    }
}

void QxtSmtpPrivate::sendBody(const QByteArray &code, const QByteArray &line)
{
    int messageID = pending.first().first;
    const QxtMailMessage &msg = pending.first().second;

    if (code[0] != '3') {
        emit qxt_p().mailFailed(messageID, code.toInt());
        emit qxt_p().mailFailed(messageID, code.toInt(), line);
        pending.removeFirst();
        sendNext();
        return;
    }

    socket->write(msg.rfc2822());
    socket->write(".\r\n");
    state = BodySent;
}
